<?php

class HOTP {
	private $secret = '123456';
	private $window = 3;
	private $counter = 0;

	public function __construct($secret = "123456", $window = 3, $counter = 0) {
		$this->secret = empty($secret) ? "123456" : $secret;
		$this->window = $window < 0 || $window > 100 ? 50 : $window;
		$this->counter = $counter;
	}

	public function gen($counter = null) {
		if ($counter === null) {
			if ($this->counter === null) {
				$this->counter = 0;
			}
			$counter = $this->counter;
			$ret = $this->gen($counter);
			$this->counter += 1;
			return $ret;
		}

		$dataStr = $this->longToString($counter);

		$bytes = $this->hmacSHA1Encrypt($dataStr);
		$num = $this->convert($bytes);
		$num = $num % 1000000;
		return sprintf("%06d", $num);
	}

	public function verify($token, $counter = null) {
		if ($counter === null) {
			if ($this->counter === null) {
				$this->counter = 0;
			}
			$counter = $this->counter;
			return $this->verify($token, $counter);
		}
		for ($i = 0; $i < $this->window; $i++) {
			{
				$tcounter = $counter - $i;
				if ($token === $this->gen($tcounter))
					return $i;
			}
			if ($i === 0)
				continue;
			{
				$tcounter = $counter + $i;
				if ($token === $this->gen($tcounter))
					return $i;
			}
		}
		return null;
	}

	private function hmacSHA1Encrypt($dataStr) {
		$secretStr = $this->secret;
		$hash = hash_hmac('sha1', $dataStr, $secretStr);
		$bytes = Array();
		foreach(str_split($hash, 2) as $hex) {
			$hmac[] = hexdec($hex);
		}
		return $hmac;
	}

	private function convert($bytes20) {
		$offset = $bytes20[19] & 0xf;
		$v = ($bytes20[$offset] & 0x7F) << 24 |
			($bytes20[$offset + 1] & 0xFF) << 16 |
			($bytes20[$offset + 2] & 0xFF) << 8  |
			($bytes20[$offset + 3] & 0xFF);
		return $v;
	}

	private function bytesToString($bytes) {
		$str = '';
		foreach($bytes as $ch) {
			$str .= chr($ch);
		}
		return $str;
	}
	private function longToBytes($v) {
		$bytes = Array();
		$vlow = $v & 0xFFFFFFFF;
		$vhigh = floor($v / 4294967296); // 没有long类型，使用除以2^32代替右移32位
		$bytes[] = (($vhigh >> 56) & 0xFF);
		$bytes[] = (($vhigh >> 48) & 0xFF);
		$bytes[] = (($vhigh >> 40) & 0xFF);
		$bytes[] = (($vhigh >> 32) & 0xFF);
		$bytes[] = (($vlow >> 24) & 0xFF);
		$bytes[] = (($vlow >> 16) & 0xFF);
		$bytes[] = (($vlow >>  8) & 0xFF);
		$bytes[] = (($vlow >>  0) & 0xFF);
		return $bytes;
	}
	private function longToString($v) {
		$bytes = $this->longToBytes($v);
		$ret = $this->bytesToString($bytes);
		return $ret;
	}
}

class TOTP {
	private $hotp;
	private $interval = 30;

	public function __construct($secret = "123456", $window = 3, $interval = 30) {
		$this->hotp = new HOTP($secret, $window, 0);
		$this->interval = $interval;
	}

	public function gen($timestamp = null) {
		$timestamp = empty($timestamp) ? (time() * 1000) : $timestamp;
		$timestamp_second = $timestamp / 1000;
		$tcounter = floor($timestamp_second / $this->interval);
		return $this->hotp->gen($tcounter);
	}

	public function gen2($timestamp = null) {
		$timestamp = empty($timestamp) ? (time() * 1000) : $timestamp;
		$timestamp_second = $timestamp / 1000;
		$tcounter = floor($timestamp_second / $this->interval);
		$token = $this->hotp->gen($tcounter);
		$step = $timestamp_second % $this->interval;
		$refresh_time = $tcounter * $this->interval;
		return array("token" => $token,
			"loop_length" => $this->interval,
			"loop_step" => $step,
			"refresh_interval" => $this->interval * 1000,
			"refresh_time" => $refresh_time * 1000,
			"timestamp" => $timestamp
			);
	}

	public function verify($token) {
		$now = time();
		$tcounter = floor($now / $this->interval);
		return $this->hotp->verify($token, $tcounter);
	}
}

/**
echo "<pre>";
$totp1 = new TOTP("hello", 3, 30);
echo json_encode($totp1->gen2(), JSON_PRETTY_PRINT);
echo $totp1->verify('975046');
echo "</pre>";
**/
